package com.beemans.topon.utils

import android.content.ClipData
import android.content.Intent
import android.os.Bundle
import android.util.Log
import androidx.annotation.IntDef
import androidx.annotation.IntRange
import androidx.collection.SimpleArrayMap
import java.lang.reflect.ParameterizedType
import java.util.*

/**
 * @author ti
 * @date 2022/11/23.
 */
object LogUtils {
    const val V = Log.VERBOSE
    const val D = Log.DEBUG
    const val I = Log.INFO
    const val W = Log.WARN
    const val E = Log.ERROR
    const val A = Log.ASSERT

    @IntDef(V, D, I, W, E, A)
    @kotlin.annotation.Retention(AnnotationRetention.SOURCE)
    annotation class TYPE

    private val LINE_SEP = System.getProperty("line.separator")
    private const val TOP_CORNER = "┌"
    private const val MIDDLE_CORNER = "├"
    private const val LEFT_BORDER = "│ "
    private const val BOTTOM_CORNER = "└"
    private const val SIDE_DIVIDER = "────────────────────────────────────────────────────────"
    private const val MIDDLE_DIVIDER = "┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄"
    private const val TOP_BORDER = TOP_CORNER + SIDE_DIVIDER + SIDE_DIVIDER
    private const val MIDDLE_BORDER = MIDDLE_CORNER + MIDDLE_DIVIDER + MIDDLE_DIVIDER
    private const val BOTTOM_BORDER = BOTTOM_CORNER + SIDE_DIVIDER + SIDE_DIVIDER
    private const val MAX_LEN = 1100 // fit for Chinese character

    private const val NOTHING = "log nothing"
    private const val NULL = "null"
    private const val ARGS = "args"
    private const val PLACEHOLDER = " "
    private val CONFIG = Config()

    private val I_FORMATTER_MAP = SimpleArrayMap<Class<*>?, IFormatter<*>>()

    fun v(vararg contents: Any?) {
        log(V, CONFIG.globalTag, *contents)
    }

    fun vTag(tag: String, vararg contents: Any?) {
        log(V, tag, *contents)
    }

    fun d(vararg contents: Any?) {
        log(D, CONFIG.globalTag, *contents)
    }

    fun dTag(tag: String, vararg contents: Any?) {
        log(D, tag, *contents)
    }

    fun i(vararg contents: Any?) {
        log(I, CONFIG.globalTag, *contents)
    }

    fun iTag(tag: String, vararg contents: Any?) {
        log(I, tag, *contents)
    }

    fun w(vararg contents: Any?) {
        log(W, CONFIG.globalTag, *contents)
    }

    fun wTag(tag: String, vararg contents: Any?) {
        log(W, tag, *contents)
    }

    fun e(vararg contents: Any?) {
        log(E, CONFIG.globalTag, *contents)
    }

    fun eTag(tag: String, vararg contents: Any?) {
        log(E, tag, *contents)
    }

    fun a(vararg contents: Any?) {
        log(A, CONFIG.globalTag, *contents)
    }

    fun aTag(tag: String, vararg contents: Any?) {
        log(A, tag, *contents)
    }

    fun log(type: Int, tag: String, vararg contents: Any?) {
        if (!CONFIG.isLogSwitch) return
        val typeLow = type and 0x0f
        if (CONFIG.isLog2ConsoleSwitch) {
            if (typeLow < CONFIG.mConsoleFilter) return
            val tagHead = processTagAndHead(tag)
            val body = processBody(*contents)
            if (CONFIG.isLog2ConsoleSwitch && typeLow >= CONFIG.mConsoleFilter) {
                print2Console(typeLow, tagHead.tag, tagHead.consoleHead, body)
            }
        }
    }

    private fun processTagAndHead(tag: String): TagHead {
        var newTag = tag
        if (!CONFIG.mTagIsSpace && !CONFIG.isLogHeadSwitch) {
            newTag = CONFIG.globalTag
        } else {
            val stackTrace = Throwable().stackTrace
            val stackIndex = 3 + CONFIG.stackOffset
            if (stackIndex >= stackTrace.size) {
                val targetElement = stackTrace[3]
                val fileName = getFileName(targetElement)
                if (CONFIG.mTagIsSpace && newTag.isBlank()) {
                    val index = fileName.indexOf('.') // Use proguard may not find '.'.
                    newTag = if (index == -1) fileName else fileName.substring(0, index)
                }
                return TagHead(newTag, null)
            }
            var targetElement = stackTrace[stackIndex]
            val fileName = getFileName(targetElement)
            if (CONFIG.mTagIsSpace && newTag.isBlank()) {
                val index = fileName.indexOf('.') // Use proguard may not find '.'.
                newTag = if (index == -1) fileName else fileName.substring(0, index)
            }
            if (CONFIG.isLogHeadSwitch) {
                val tName = Thread.currentThread().name
                val head = Formatter()
                    .format(
                        "%s, %s.%s(%s:%d)",
                        tName,
                        targetElement.className,
                        targetElement.methodName,
                        fileName,
                        targetElement.lineNumber
                    )
                    .toString()
                return if (CONFIG.stackDeep <= 1) {
                    TagHead(newTag, arrayOf(head))
                } else {
                    val consoleHead = arrayOfNulls<String>(
                        CONFIG.stackDeep.coerceAtMost(stackTrace.size - stackIndex)
                    )
                    consoleHead[0] = head
                    val spaceLen = tName.length + 2
                    val space = Formatter().format("%" + spaceLen + "s", "").toString()
                    var i = 1
                    val len = consoleHead.size
                    while (i < len) {
                        targetElement = stackTrace[i + stackIndex]
                        consoleHead[i] = Formatter()
                            .format(
                                "%s%s.%s(%s:%d)",
                                space,
                                targetElement.className,
                                targetElement.methodName,
                                getFileName(targetElement),
                                targetElement.lineNumber
                            )
                            .toString()
                        ++i
                    }
                    TagHead(newTag, consoleHead)
                }
            }
        }
        return TagHead(newTag, null)
    }

    private fun getFileName(targetElement: StackTraceElement): String {
        val fileName = targetElement.fileName
        if (fileName != null) return fileName
        // If name of file is null, should add
        // "-keepattributes SourceFile,LineNumberTable" in proguard file.
        var className = targetElement.className
        val classNameInfo = className.split("\\.".toRegex()).toTypedArray()
        if (classNameInfo.isNotEmpty()) {
            className = classNameInfo[classNameInfo.size - 1]
        }
        val index = className.indexOf('$')
        if (index != -1) {
            className = className.substring(0, index)
        }
        return "$className.java"
    }

    private fun processBody(vararg contents: Any?): String {
        val body = if (contents.size == 1) {
            formatObject(contents[0])
        } else {
            val sb = StringBuilder()
            var i = 0
            val len = contents.size
            while (i < len) {
                val content = contents[i]
                sb.append(ARGS)
                    .append("[")
                    .append(i)
                    .append("]")
                    .append(" = ")
                    .append(formatObject(content))
                    .append(LINE_SEP)
                ++i
            }
            sb.toString()
        }
        return body.ifEmpty { NOTHING }
    }

    @Suppress("UNCHECKED_CAST")
    private fun formatObject(`object`: Any?): String {
        if (`object` == null) return NULL
        if (!I_FORMATTER_MAP.isEmpty) {
            val iFormatter = I_FORMATTER_MAP[getClassFromObject(`object`)] as? IFormatter<Any>
            if (iFormatter != null) {
                return iFormatter.format<Any>(`object`)
            }
        }
        return LogFormatter.object2String(`object`)
    }

    private fun print2Console(
        type: Int,
        tag: String,
        head: Array<String?>?,
        msg: String
    ) {
        if (CONFIG.isSingleTagSwitch) {
            printSingleTagMsg(type, tag, processSingleTagMsg(head, msg))
        } else {
            printBorder(type, tag, true)
            printHead(type, tag, head)
            printMsg(type, tag, msg)
            printBorder(type, tag, false)
        }
    }

    private fun printBorder(type: Int, tag: String, isTop: Boolean) {
        if (CONFIG.isLogBorderSwitch) {
            print2Console(type, tag, if (isTop) TOP_BORDER else BOTTOM_BORDER)
        }
    }

    private fun printHead(type: Int, tag: String, head: Array<String?>?) {
        if (head != null) {
            for (aHead in head) {
                print2Console(
                    type, tag,
                    (if (CONFIG.isLogBorderSwitch) LEFT_BORDER + aHead else aHead) ?: ""
                )
            }
            if (CONFIG.isLogBorderSwitch) print2Console(type, tag, MIDDLE_BORDER)
        }
    }

    private fun printMsg(type: Int, tag: String, msg: String) {
        val len = msg.length
        val countOfSub = len / MAX_LEN
        if (countOfSub > 0) {
            var index = 0
            for (i in 0 until countOfSub) {
                printSubMsg(type, tag, msg.substring(index, index + MAX_LEN))
                index += MAX_LEN
            }
            if (index != len) {
                printSubMsg(type, tag, msg.substring(index, len))
            }
        } else {
            printSubMsg(type, tag, msg)
        }
    }

    private fun printSubMsg(type: Int, tag: String, msg: String) {
        if (!CONFIG.isLogBorderSwitch) {
            print2Console(type, tag, msg)
            return
        }
        val lines = msg.split(LINE_SEP?.toRegex() ?: Regex("")).toTypedArray()
        for (line in lines) {
            print2Console(type, tag, LEFT_BORDER + line)
        }
    }

    private fun processSingleTagMsg(head: Array<String?>?, msg: String): String {
        val sb = StringBuilder()
        if (CONFIG.isLogBorderSwitch) {
            sb.append(PLACEHOLDER).append(LINE_SEP)
            sb.append(TOP_BORDER).append(LINE_SEP)
            if (head != null) {
                for (aHead in head) {
                    sb.append(LEFT_BORDER).append(aHead).append(LINE_SEP)
                }
                sb.append(MIDDLE_BORDER).append(LINE_SEP)
            }
            for (line in msg.split(LINE_SEP?.toRegex() ?: Regex("")).toTypedArray()) {
                sb.append(LEFT_BORDER).append(line).append(LINE_SEP)
            }
            sb.append(BOTTOM_BORDER)
        } else {
            if (head != null) {
                sb.append(PLACEHOLDER).append(LINE_SEP)
                for (aHead in head) {
                    sb.append(aHead).append(LINE_SEP)
                }
            }
            sb.append(msg)
        }
        return sb.toString()
    }

    private fun printSingleTagMsg(type: Int, tag: String, msg: String) {
        val len = msg.length
        val countOfSub =
            if (CONFIG.isLogBorderSwitch) (len - BOTTOM_BORDER.length) / MAX_LEN else len / MAX_LEN
        if (countOfSub > 0) {
            if (CONFIG.isLogBorderSwitch) {
                print2Console(type, tag, msg.substring(0, MAX_LEN) + LINE_SEP + BOTTOM_BORDER)
                var index = MAX_LEN
                for (i in 1 until countOfSub) {
                    print2Console(
                        type, tag, PLACEHOLDER + LINE_SEP + TOP_BORDER + LINE_SEP
                                + LEFT_BORDER + msg.substring(index, index + MAX_LEN)
                                + LINE_SEP + BOTTOM_BORDER
                    )
                    index += MAX_LEN
                }
                if (index != len - BOTTOM_BORDER.length) {
                    print2Console(
                        type, tag, PLACEHOLDER + LINE_SEP + TOP_BORDER + LINE_SEP
                                + LEFT_BORDER + msg.substring(index, len)
                    )
                }
            } else {
                print2Console(type, tag, msg.substring(0, MAX_LEN))
                var index = MAX_LEN
                for (i in 1 until countOfSub) {
                    print2Console(
                        type, tag,
                        PLACEHOLDER + LINE_SEP + msg.substring(index, index + MAX_LEN)
                    )
                    index += MAX_LEN
                }
                if (index != len) {
                    print2Console(type, tag, PLACEHOLDER + LINE_SEP + msg.substring(index, len))
                }
            }
        } else {
            print2Console(type, tag, msg)
        }
    }

    private fun print2Console(type: Int, tag: String, msg: String) {
        Log.println(type, tag, msg)
    }

    class Config {
        var isLogSwitch = true // The switch of log.
            private set
        var isLog2ConsoleSwitch = true // The logcat's switch of log.
            private set
        private var mGlobalTag = "" // The global tag of log.
        var mTagIsSpace = true // The global tag is space.
            private set
        var isLogHeadSwitch = true // The head's switch of log.
            private set
        var isLogBorderSwitch = true // The border's switch of log.
            private set
        var isSingleTagSwitch = true // The single tag of log.
            private set
        var mConsoleFilter = V // The console's filter of log.
            private set
        var stackDeep = 1 // The stack's deep of log.
            private set
        var stackOffset = 0 // The stack's offset of log.
            private set

        fun setLogSwitch(logSwitch: Boolean): Config {
            isLogSwitch = logSwitch
            return this
        }

        fun setConsoleSwitch(consoleSwitch: Boolean): Config {
            isLog2ConsoleSwitch = consoleSwitch
            return this
        }

        fun setBorderSwitch(borderSwitch: Boolean): Config {
            isLogBorderSwitch = borderSwitch
            return this
        }

        fun setSingleTagSwitch(singleTagSwitch: Boolean): Config {
            isSingleTagSwitch = singleTagSwitch
            return this
        }

        fun setConsoleFilter(@TYPE consoleFilter: Int): Config {
            mConsoleFilter = consoleFilter
            return this
        }

        fun setStackDeep(@IntRange(from = 1) stackDeep: Int): Config {
            this.stackDeep = stackDeep
            return this
        }

        fun setStackOffset(@IntRange(from = 0) stackOffset: Int): Config {
            this.stackOffset = stackOffset
            return this
        }

        fun <T> addFormatter(iFormatter: IFormatter<T>?): Config {
            if (iFormatter != null) {
                I_FORMATTER_MAP.put(getTypeClassFromParadigm(iFormatter), iFormatter)
            }
            return this
        }

        val globalTag: String
            get() = mGlobalTag.ifBlank { "" }

        override fun toString(): String {
            return ("logSwitch: " + isLogSwitch
                    + LINE_SEP + "consoleSwitch: " + isLog2ConsoleSwitch
                    + LINE_SEP + "tag: " + (if (globalTag == "") "null" else globalTag)
                    + LINE_SEP + "headSwitch: " + isLogHeadSwitch
                    + LINE_SEP + "borderSwitch: " + isLogBorderSwitch
                    + LINE_SEP + "singleTagSwitch: " + isSingleTagSwitch
                    + LINE_SEP + "stackDeep: " + stackDeep
                    + LINE_SEP + "stackOffset: " + stackOffset
                    + LINE_SEP + "formatter: " + I_FORMATTER_MAP
                    )
        }
    }

    abstract class IFormatter<T> {
        abstract fun <U> format(t: T): String
    }

    private class TagHead(
        var tag: String,
        var consoleHead: Array<String?>?,
    )

    private object LogFormatter {
        fun object2String(`object`: Any): String {
            if (`object`.javaClass.isArray) return array2String(`object`)
            if (`object` is Throwable) return ThrowableUtils.getFullStackTrace(`object`)
            if (`object` is Bundle) return bundle2String(`object`)
            if (`object` is Intent) return intent2String(`object`)
            return `object`.toString()
        }

        private fun bundle2String(bundle: Bundle): String {
            val iterator = bundle.keySet().iterator()
            if (!iterator.hasNext()) {
                return "Bundle {}"
            }
            val sb = StringBuilder(128)
            sb.append("Bundle { ")
            while (true) {
                val key = iterator.next()
                val value = bundle[key]
                sb.append(key).append('=')
                if (value is Bundle) {
                    sb.append(if (value === bundle) "(this Bundle)" else bundle2String(value))
                } else {
                    sb.append(formatObject(value))
                }
                if (!iterator.hasNext()) return sb.append(" }").toString()
                sb.append(',').append(' ')
            }
        }

        private fun intent2String(intent: Intent): String {
            val sb = StringBuilder(128)
            sb.append("Intent { ")
            var first = true
            val mAction = intent.action
            if (mAction != null) {
                sb.append("act=").append(mAction)
                first = false
            }
            val mCategories = intent.categories
            if (mCategories != null) {
                if (!first) {
                    sb.append(' ')
                }
                first = false
                sb.append("cat=[")
                var firstCategory = true
                for (c in mCategories) {
                    if (!firstCategory) {
                        sb.append(',')
                    }
                    sb.append(c)
                    firstCategory = false
                }
                sb.append("]")
            }
            val mData = intent.data
            if (mData != null) {
                if (!first) {
                    sb.append(' ')
                }
                first = false
                sb.append("dat=").append(mData)
            }
            val mType = intent.type
            if (mType != null) {
                if (!first) {
                    sb.append(' ')
                }
                first = false
                sb.append("typ=").append(mType)
            }
            val mFlags = intent.flags
            if (mFlags != 0) {
                if (!first) {
                    sb.append(' ')
                }
                first = false
                sb.append("flg=0x").append(Integer.toHexString(mFlags))
            }
            val mPackage = intent.getPackage()
            if (mPackage != null) {
                if (!first) {
                    sb.append(' ')
                }
                first = false
                sb.append("pkg=").append(mPackage)
            }
            val mComponent = intent.component
            if (mComponent != null) {
                if (!first) {
                    sb.append(' ')
                }
                first = false
                sb.append("cmp=").append(mComponent.flattenToShortString())
            }
            val mSourceBounds = intent.sourceBounds
            if (mSourceBounds != null) {
                if (!first) {
                    sb.append(' ')
                }
                first = false
                sb.append("bnds=").append(mSourceBounds.toShortString())
            }
            val mClipData = intent.clipData
            if (mClipData != null) {
                if (!first) {
                    sb.append(' ')
                }
                first = false
                clipData2String(mClipData, sb)
            }
            val mExtras = intent.extras
            if (mExtras != null) {
                if (!first) {
                    sb.append(' ')
                }
                first = false
                sb.append("extras={")
                sb.append(bundle2String(mExtras))
                sb.append('}')
            }
            val mSelector = intent.selector
            if (mSelector != null) {
                if (!first) {
                    sb.append(' ')
                }
                sb.append("sel={")
                sb.append(if (mSelector === intent) "(this Intent)" else intent2String(mSelector))
                sb.append("}")
            }
            sb.append(" }")
            return sb.toString()
        }

        private fun clipData2String(clipData: ClipData, sb: StringBuilder) {
            val item = clipData.getItemAt(0)
            if (item == null) {
                sb.append("ClipData.Item {}")
                return
            }
            sb.append("ClipData.Item { ")
            val mHtmlText = item.htmlText
            if (mHtmlText != null) {
                sb.append("H:")
                sb.append(mHtmlText)
                sb.append("}")
                return
            }
            val mText = item.text
            if (mText != null) {
                sb.append("T:")
                sb.append(mText)
                sb.append("}")
                return
            }
            val uri = item.uri
            if (uri != null) {
                sb.append("U:").append(uri)
                sb.append("}")
                return
            }
            val intent = item.intent
            if (intent != null) {
                sb.append("I:")
                sb.append(intent2String(intent))
                sb.append("}")
                return
            }
            sb.append("NULL")
            sb.append("}")
        }

        private fun array2String(`object`: Any?): String {
            when (`object`) {
                is Array<*> -> {
                    return Arrays.deepToString(`object` as? Array<*>)
                }
                is BooleanArray -> {
                    return Arrays.toString(`object`)
                }
                is ByteArray -> {
                    return Arrays.toString(`object`)
                }
                is CharArray -> {
                    return Arrays.toString(`object`)
                }
                is DoubleArray -> {
                    return Arrays.toString(`object`)
                }
                is FloatArray -> {
                    return Arrays.toString(`object`)
                }
                is IntArray -> {
                    return Arrays.toString(`object`)
                }
                is LongArray -> {
                    return Arrays.toString(`object`)
                }
                is ShortArray -> {
                    return Arrays.toString(`object`)
                }
                else -> throw IllegalArgumentException("Array has incompatible type: " + `object`?.javaClass)
            }
        }
    }

    private fun <T> getTypeClassFromParadigm(formatter: IFormatter<T>): Class<*>? {
        val genericInterfaces = formatter.javaClass.genericInterfaces
        var type = if (genericInterfaces.size == 1) {
            genericInterfaces[0]
        } else {
            formatter.javaClass.genericSuperclass
        }
        type = (type as? ParameterizedType)?.actualTypeArguments?.get(0)
        while (type is ParameterizedType) {
            type = type.rawType
        }
        var className = type.toString()
        if (className.startsWith("class ")) {
            className = className.substring(6)
        } else if (className.startsWith("interface ")) {
            className = className.substring(10)
        }
        try {
            return Class.forName(className)
        } catch (e: ClassNotFoundException) {
            e.printStackTrace()
        }
        return null
    }

    private fun getClassFromObject(obj: Any): Class<*>? {
        val objClass: Class<*> = obj.javaClass
        if (objClass.isAnonymousClass || objClass.isSynthetic) {
            val genericInterfaces = objClass.genericInterfaces
            var className: String
            if (genericInterfaces.size == 1) { // interface
                var type = genericInterfaces[0]
                while (type is ParameterizedType) {
                    type = type.rawType
                }
                className = type.toString()
            } else { // abstract class or lambda
                var type = objClass.genericSuperclass
                while (type is ParameterizedType) {
                    type = type.rawType
                }
                className = type?.toString() ?: ""
            }
            if (className.startsWith("class ")) {
                className = className.substring(6)
            } else if (className.startsWith("interface ")) {
                className = className.substring(10)
            }
            try {
                return Class.forName(className)
            } catch (e: ClassNotFoundException) {
                e.printStackTrace()
            }
        }
        return objClass
    }
}